package com.db4o.odbgen.plugins.java.enhancements;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;

import com.db4o.internal.odbgen.TemplatesUtils;
import com.db4o.internal.odbgen.plugins.TemplateList;
import com.db4o.odbgen.OdbgenException;
import com.db4o.odbgen.TargetLanguage;
import com.db4o.odbgen.plugins.MainGeneratorPlugin;
import com.db4o.odbgen.plugins.Plugin;
import com.db4o.odbgen.plugins.java.maingenerator.MainGenerator;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.CodeStorage;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSClass;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSField;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSInterface;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSModifier;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.xtree.XTEField;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.xtree.XTreeElement;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.xtree.XTEClass;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.xtree.XTree;

//TODO:
//Move test5 to unit testing
//Remove any TODOS related to soda graph.
//Add documentation inside user manual


/**
 * Generates the SodaGraph hierarchy that allows performing type safe SODA queries. 
 * @author liviug
 */
public class SodaGraph implements Plugin{

	private MainGenerator _mainGenerator;

	/**
	 * Contains the list of standard templates used by {@link SodaGraph}.
	 * @author liviug
	 */
	private enum Templates implements TemplateList{
		SGCLASS(Templates.path, "sgClass.vm");
		private static final String path = "standardPlugins/java/enhancements/sodaGraph";

		
		private String _path;
		private String _name;

		Templates(String path, String name){
			this._path = path;
			this._name = name;
		}
		
		/**
		 * Returns the path and the name of the template.
		 */
		public String getQualifiedName() {
			return this._path+"/"+this._name;
		}
	}
	
	/**
	 * See {@link Plugin} for details.
	 */
	@Override
	public List<Class<? extends Plugin>> getPluginDependencies() {
		ArrayList<Class<? extends Plugin>> res = new ArrayList<Class<? extends Plugin>>();
		res.add(MainGenerator.class);
		return res;
	}

	/**
	 * See {@link Plugin} for details.
	 */
	@Override
	public void initialize(MainGeneratorPlugin mainGenerator) {
		this._mainGenerator = (MainGenerator)mainGenerator;
		
	}

	/**
	 * See {@link Plugin} for details.
	 */
	@Override
	public void start() throws OdbgenException {
		
		//Generate SodaGraph for all classes XmlSchema.
		XTree xtree = new XTree(this._mainGenerator.getTypedXmlSchema(), 2, true);
		for(XTreeElement rootElem : xtree.getRootElements()){
			if (rootElem instanceof XTEClass) {
				XTEClass rootClass = (XTEClass) rootElem;
				Map<String, String> imports = new HashMap<String, String>();
				String sgClassContent = this.generateSodaGraphClassContent(
						rootClass,
						new ArrayList<String>(),
						imports, 
						true, 
						null);
				TSClass tsClass = new TSClass(
						null,
						this.calculateSodaGraphPackageName(rootClass.getTsClass()),
						TSModifier.getPublicModifier(),
						this.calculateSodaGraphClassName(rootClass.getTsClass()),
						new ArrayList<TSField>(),
						null,
						new ArrayList<TSInterface>()
						);
				tsClass.getAdditionalContent().append(sgClassContent);
				tsClass.getImports().add("com.db4o.query.Constraint");
				tsClass.getImports().add("com.db4o.query.Query");
				//Add the imports copied from the original classes.
				for(String imp : imports.values()){
					tsClass.getImports().add(imp);
				}
				
				this._mainGenerator.getTypedXmlSchema().getTypeDeclarations().add(tsClass);
			}
		}
	}


	/**
	 * Returns the name of a top level SodaGraph class name given the corresponding XmlSchema class.
	 * @param tsClass The XmlSchema class.
	 * @return
	 */
	private String calculateSodaGraphClassName(TSClass tsClass) {
		return "Sg"+tsClass.getSimpleName();
	}

	/**
	 * Returns the name of a non top level SodaGraph class name given the corresponding XmlSchema class.
	 * @param tsClass The XmlSchema class.
	 * @return
	 */
	private String calculateSodaGraphInnerClassName(TSClass tsClass) {
		return tsClass.getSimpleName()+"Field_"+UUID.randomUUID().toString().substring(0, 4);
	}
	
	/**
	 * Returns the package for a top level SodaGraph class given the corresponding XmlSchema class.
	 * @param tsClass The XmlSchema class.
	 * @return
	 */
	private String calculateSodaGraphPackageName(TSClass tsClass) {
		String pck = tsClass.getPackage();
		if(pck.isEmpty()){
			return "sodaGraph";
		}
		else{
			return pck+".sodaGraph";
		}
	}

	/**
	 * Generates the SodaGraph for the specified class.
	 * @param rootClass The class to generate SodaGraph for.
	 * @param fieldPath See {@link SodaGraphField}.getFieldPath() for details.
	 * @param imports The imports in the original classes will be dupplicated
	 * in the SodaGraph classes.
	 * @param topLevelClass This method can generate code for both top level SodaGraph classes and
	 * non top level SodaGraph classes.
	 * @param innerClassName This is the name of the inner class. This is send each time a non top
	 * level class is processed (topLevelClass is always false when this parameter is specified).
	 * For top level classes, this parameter is not specified.
	 * The reason for this parameter is that the name of the inner class is generated using a random seed and
	 * we must call the method that generates it exactly once per each inner class.
	 * @throws OdbgenException 
	 */
	private String generateSodaGraphClassContent(
			XTEClass rootClass, 
			List<String> fieldPath,
			Map<String, String> imports, 
			boolean topLevelClass,
			String innerClassName
			) throws OdbgenException {
		TSClass sourceClass = rootClass.getTsClass();
		String pkg = sourceClass.getPackage();
		List<SodaGraphField> sdFields = new ArrayList<SodaGraphField>();

//TODO: remove this
//if(sourceClass.getSimpleName().equals("User")){
//			System.out.println("");
//}
		
		
		//Add the imports.
		for(String imp : sourceClass.getImports()){
			if(!imports.containsKey(imp)){
				imports.put(imp, imp);
			}
		}
		//Add the import representing all classes from the package of the
		//original class. This is because the original class has access
		//to all classes withinthe package without importing anything.
		String allImports = String.format("%s.*", rootClass.getTsClass().getPackage());
		imports.put(allImports, allImports);
		
		//Iterate all fields of the class.
		for(XTreeElement xtelem : rootClass.getChildren()){
			if (xtelem instanceof XTEField) {
				XTEField xtfield = (XTEField)xtelem;
				
				//Add the field to the path.
				fieldPath.add(xtfield.getField().getName());

				//Create and populate a SodaGraph field.
				SodaGraphField sdField = new SodaGraphField();
				sdFields.add(sdField);
				
				sdField.setField(xtfield.getField());
				sdField.getFieldPath().addAll(fieldPath);
				
				//Check whether this field's type is a class part of XmlSchema.
				XTreeElement xtchild = xtfield.getChild();
				if(xtchild!=null && xtchild instanceof XTEClass){
					XTEClass childClass = (XTEClass) xtchild;

					//Build the non top level SodaGraph inner class associated with
					//xtField.
					String childInnerClassName = this.calculateSodaGraphInnerClassName(childClass.getTsClass());
					TSClass sgInnerClass = new TSClass(
							null,
							"",
							TSModifier.getPublicStaticModifier(),
							childInnerClassName,
							new ArrayList<TSField>(),
							null,
							new ArrayList<TSInterface>()
							);
					
					//We build this class by adding additional content. We are not building
					//the class adding by adding individual fields and methods.
					String sgInnerClassContent = this.generateSodaGraphClassContent(
							childClass,
							fieldPath,
							imports, 
							false, 
							childInnerClassName
							);
					sgInnerClass.getAdditionalContent().append(sgInnerClassContent);
					
					sdField.setAssociatedSgInnerClass(sgInnerClass);
					sdField.setAssociatedSgInnerClassContent(CodeStorage.generateClassContents(
							this._mainGenerator.getStandardTemplatesJarName(), 
							sgInnerClass, 
							true
							));
				}
				
				//Remove the field from the path. This means removing the last element.
				fieldPath.remove(fieldPath.size()-1);
			}
	
		}
		
		//Build the content of the SodaGraph class.
		Template template = TemplatesUtils.getTemplateJar(SodaGraph.Templates.SGCLASS.getQualifiedName(), this._mainGenerator.getStandardTemplatesJarName());
		VelocityContext context = new VelocityContext();
		context.put("package", pkg);
		context.put("sourceClass", sourceClass);
		context.put("sodaGraphFields", sdFields);
		context.put("isTopLevelClass", topLevelClass?"true":"false");
		
		if(topLevelClass){
			context.put("sgClassName", this.calculateSodaGraphClassName(rootClass.getTsClass()));
		}
		else{
			context.put("sgClassName", innerClassName);
		}
		String content = TemplatesUtils.merge(template, context);

		return content;
	}

	/**
	 * See {@link Plugin} for details.
	 */
	@Override
	public boolean supportsTargetLanguage(TargetLanguage targetLanguage) {
		if(targetLanguage == TargetLanguage.JAVA){
			return true;
		}
		return false;
	}

}
